<!--
 * @Date: 2025-12-02 16:30:00
 * @Description: 使用原生 Canvas API 绘制的3D饼图组件
 * @LastEditTime: 2025-12-03 11:12:42
-->
<template>
  <div class="canvas-pie-container">
    <canvas ref="canvasRef" :width="canvasWidth" :height="canvasHeight" @mousemove="handleMouseMove"
      @mouseout="handleMouseOut" @mouseenter="handleMouseEnter"></canvas>
    <!-- 鼠标移入的提示框 -->
    <div v-if="tooltip.visible" class="tooltip" :style="{ top: tooltip.top + 'px', left: tooltip.left + 'px' }">
      <div class="tooltip-title">
        <span class="tooltip-legend" :style="{ backgroundColor: tooltip.color }"></span>
        <span class="tooltip-name">{{ tooltip.name }}</span>
      </div>
      <div class="tooltip-value">{{ tooltip.value }}</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CanvasPie3D',
  props: {
    list: {
      type: Array,
      default: () => []
    },
    // 饼图半径缩放比例，控制饼图大小 (0-1)
    radiusScale: {
      type: Number,
      default: 0.28
    },
    // 透视角度，值越小，3D效果越扁平 (0-1)
    perspective: {
      type: Number,
      default: 0.5
    },
    // 最大扇区高度
    maxPieHeight: {
      type: Number,
      default: 25
    },
    // 最小扇区高度
    minPieHeight: {
      type: Number,
      default: 10
    },
    // **重要**: 设置为 true 来开启提示框轮播功能
    enableCarousel: {
      type: Boolean,
      default: false
    },
    // 轮播间隔（毫秒）
    carouselInterval: {
      type: Number,
      default: 3000
    }
  },
  data() {
    return {
      canvas: null,
      ctx: null,
      resizeObserver: null,
      slices: [],
      hoveredSlice: null,
      tooltip: {
        visible: false,
        name: '',
        value: '',
        color: '',
        top: 0,
        left: 0
      },
      carouselTimer: null,
      carouselIndex: -1,
      isMouseOver: false,
      canvasWidth: 0,
      canvasHeight: 0
    }
  },
  watch: {
    list: { handler: 'prepareDataAndDraw', deep: true, immediate: true },
    radiusScale: { handler: 'prepareDataAndDraw' },
    perspective: { handler: 'prepareDataAndDraw' },
    enableCarousel(newVal) {
      if (newVal && !this.isMouseOver) {
        this.startCarousel()
      } else {
        this.stopCarousel()
      }
    }
  },
  mounted() {
    this.initCanvas()
    this.resizeObserver = new ResizeObserver(this.handleResize)
    this.resizeObserver.observe(this.$el)
  },
  beforeDestroy() {
    if (this.resizeObserver) {
      this.resizeObserver.disconnect() // 1. 添加此行，断开观察者
    }
    this.stopCarousel()
  },
  methods: {
    initCanvas() {
      this.canvas = this.$refs.canvasRef
      this.ctx = this.canvas.getContext('2d')
      this.handleResize()
    },
    handleResize() {
      const container = this.$el
      if (!container || !this.canvas) return

      // 1. 使用 offsetWidth/offsetHeight 获取布局尺寸，忽略 transform 缩放
      const width = container.offsetWidth
      const height = container.offsetHeight

      // 增加一个判断，避免在元素尚未布局时执行
      if (width === 0 || height === 0) {
        return
      }

      const dpr = window.devicePixelRatio || 1
      this.canvas.width = width * dpr
      this.canvas.height = height * dpr
      this.canvas.style.width = `${width}px`
      this.canvas.style.height = `${height}px`
      this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
      this.prepareDataAndDraw()
    },
    adjustColor(hex, percent) {
      let r = parseInt(hex.substring(1, 3), 16)
      let g = parseInt(hex.substring(3, 5), 16)
      let b = parseInt(hex.substring(5, 7), 16)
      const amount = Math.round(2.55 * percent)
      r = Math.max(0, Math.min(255, r + amount))
      g = Math.max(0, Math.min(255, g + amount))
      b = Math.max(0, Math.min(255, b + amount))
      return `rgb(${r}, ${g}, ${b})`
    },
    prepareDataAndDraw() {
      if (!this.ctx || !this.list || this.list.length === 0) return
      const data = this.list.map(item => ({
        name: item.goodsName || item.name,
        value: item.goodsAmount || item.value
      }))
      const colors = ['#FFC700', '#9A66FF', '#2CFE9D', '#0075F4', '#00d8ff', '#00E676', '#6200EA']
      const total = data.reduce((sum, item) => sum + item.value, 0)
      const values = data.map(d => d.value)
      const minVal = Math.min(...values)
      const maxVal = Math.max(...values)
      let currentAngle = -Math.PI / 2

      const mappedSlices = data.map((item, index) => {
        const sliceAngle = (item.value / total) * 2 * Math.PI
        const startAngle = currentAngle
        currentAngle += sliceAngle
        const endAngle = currentAngle
        const midAngle = startAngle + sliceAngle / 2
        let height = this.minPieHeight
        if (maxVal > minVal) {
          height =
            this.minPieHeight +
            ((item.value - minVal) / (maxVal - minVal)) * (this.maxPieHeight - this.minPieHeight)
        }
        return { ...item, startAngle, endAngle, midAngle, height, color: colors[index % colors.length] }
      })

      // **核心修改 2: 采用更精确的画家算法排序**
      // 根据扇区中点的正弦值（Y轴投影，即深度）进行排序，从后到前绘制
      this.slices = mappedSlices.sort((a, b) => Math.sin(a.midAngle) - Math.sin(b.midAngle))

      this.draw()
      this.stopCarousel()
      if (this.enableCarousel && !this.isMouseOver) {
        this.startCarousel()
      }
    },
    draw() {
      if (!this.ctx) return
      // 2. 在 draw 函数中同样使用 offsetWidth/offsetHeight 保持一致
      const width = this.$el.offsetWidth
      const height = this.$el.offsetHeight
      this.ctx.clearRect(0, 0, width, height)

      const radius = Math.min(width, height) * this.radiusScale
      const centerX = width / 2
      const centerY = height / 2 + this.maxPieHeight
      const yScale = this.perspective

      this.slices.forEach(slice => {
        const { startAngle, endAngle, color, height } = slice
        const isHovered = this.hoveredSlice === slice
        const topY = centerY - height
        const mainColor = isHovered ? this.adjustColor(color, 20) : color
        const sideColor = this.adjustColor(color, -20)
        const darkSideColor = this.adjustColor(color, -40)

        // 绘制底面
        this.ctx.fillStyle = darkSideColor
        this.ctx.beginPath()
        this.ctx.moveTo(centerX, centerY)
        this.ctx.ellipse(centerX, centerY, radius, radius * yScale, 0, startAngle, endAngle)
        this.ctx.closePath()
        this.ctx.fill()

        // 绘制起始角度的垂直切面
        this.ctx.fillStyle = sideColor
        this.ctx.beginPath()
        this.ctx.moveTo(centerX, centerY)
        this.ctx.lineTo(centerX, topY)
        this.ctx.lineTo(centerX + Math.cos(startAngle) * radius, topY + Math.sin(startAngle) * radius * yScale)
        this.ctx.lineTo(centerX + Math.cos(startAngle) * radius, centerY + Math.sin(startAngle) * radius * yScale)
        this.ctx.closePath()
        this.ctx.fill()

        // 绘制结束角度的垂直切面
        this.ctx.fillStyle = sideColor
        this.ctx.beginPath()
        this.ctx.moveTo(centerX, centerY)
        this.ctx.lineTo(centerX, topY)
        this.ctx.lineTo(centerX + Math.cos(endAngle) * radius, topY + Math.sin(endAngle) * radius * yScale)
        this.ctx.lineTo(centerX + Math.cos(endAngle) * radius, centerY + Math.sin(endAngle) * radius * yScale)
        this.ctx.closePath()
        this.ctx.fill()

        // **核心修改 1: 简化侧面绘制，移除纹理线条**
        // 仅用平滑的纯色填充外弧面
        const sidePath = new Path2D()
        sidePath.ellipse(centerX, centerY, radius, radius * yScale, 0, startAngle, endAngle)
        sidePath.ellipse(centerX, topY, radius, radius * yScale, 0, endAngle, startAngle, true)
        sidePath.closePath()
        this.ctx.fillStyle = sideColor
        this.ctx.fill(sidePath)

        // 绘制顶面
        this.ctx.fillStyle = mainColor
        this.ctx.beginPath()
        this.ctx.moveTo(centerX, topY)
        this.ctx.ellipse(centerX, topY, radius, radius * yScale, 0, startAngle, endAngle)
        this.ctx.closePath()
        this.ctx.fill()
      })

      this.drawLabels()
    },
    drawLabels() {
      // 3. 在 drawLabels 函数中也使用 offsetWidth/offsetHeight
      const width = this.$el.offsetWidth
      const height = this.$el.offsetHeight
      const radius = Math.min(width, height) * this.radiusScale
      const centerX = width / 2
      const centerY = height / 2 + this.maxPieHeight
      const yScale = this.perspective
      // **核心修改 3: 增加标签间距**
      const labelMinSpacing = 28 // 进一步增加最小间距以拉开距离
      const labelLineMargin = 25

      const labels = this.slices.map(slice => {
        const topY = centerY - slice.height
        const lineEndY = topY + Math.sin(slice.midAngle) * (radius + 30) * yScale
        return { y: lineEndY, slice: slice, finalY: lineEndY }
      })

      const rightLabels = labels.filter(l => l.slice.midAngle > -Math.PI / 2 && l.slice.midAngle < Math.PI / 2).sort((a, b) => a.y - b.y)
      const leftLabels = labels.filter(l => !(l.slice.midAngle > -Math.PI / 2 && l.slice.midAngle < Math.PI / 2)).sort((a, b) => a.y - b.y)

      const layout = (labelGroup) => {
        for (let i = 1; i < labelGroup.length; i++) {
          const prev = labelGroup[i - 1]
          const curr = labelGroup[i]
          if (curr.finalY < prev.finalY + labelMinSpacing) {
            curr.finalY = prev.finalY + labelMinSpacing
          }
        }
        for (let i = labelGroup.length - 2; i >= 0; i--) {
          const next = labelGroup[i + 1]
          const curr = labelGroup[i]
          if (curr.finalY > next.finalY - labelMinSpacing) {
            curr.finalY = next.finalY - labelMinSpacing
          }
        }
      }
      layout(rightLabels)
      layout(leftLabels)

      labels.forEach(label => {
        const { name, value, midAngle, color, height } = label.slice
        const topY = centerY - height
        const isRightSide = midAngle > -Math.PI / 2 && midAngle < Math.PI / 2
        const lineStartX = centerX + Math.cos(midAngle) * radius
        const lineStartY = topY + Math.sin(midAngle) * radius * yScale
        const lineEndX = centerX + Math.cos(midAngle) * (radius + 30)
        const lineEndY = label.finalY

        this.ctx.strokeStyle = color
        this.ctx.lineWidth = 1
        this.ctx.beginPath()
        this.ctx.moveTo(lineStartX, lineStartY)
        this.ctx.lineTo(lineEndX, lineEndY)
        this.ctx.lineTo(lineEndX + (isRightSide ? 20 : -20), lineEndY)
        this.ctx.stroke()

        this.ctx.fillStyle = '#fff'
        this.ctx.font = '14px Arial'
        this.ctx.textAlign = isRightSide ? 'left' : 'right'
        this.ctx.textBaseline = 'bottom'
        this.ctx.fillText(`${name}`, lineEndX + (isRightSide ? labelLineMargin : -labelLineMargin), lineEndY - 2)
        this.ctx.font = '12px Arial'
        this.ctx.textBaseline = 'top'
        this.ctx.fillText(`${value}`, lineEndX + (isRightSide ? labelLineMargin : -labelLineMargin), lineEndY + 2)
      })
    },
    getSliceHitPath(slice) {
      const { width, height } = this.$el.getBoundingClientRect()
      const radius = Math.min(width, height) * this.radiusScale
      const centerX = width / 2
      const centerY = height / 2 + this.maxPieHeight
      const yScale = this.perspective
      const topY = centerY - slice.height

      const path = new Path2D()
      path.ellipse(centerX, topY, radius, radius * yScale, 0, slice.startAngle, slice.endAngle)
      path.lineTo(centerX + Math.cos(slice.endAngle) * radius, centerY + Math.sin(slice.endAngle) * radius * yScale)
      path.ellipse(centerX, centerY, radius, radius * yScale, 0, slice.endAngle, slice.startAngle, true)
      path.closePath()
      return path
    },
    handleMouseEnter() {
      this.isMouseOver = true
      this.stopCarousel()
    },
    handleMouseMove(event) {
      const rect = this.canvas.getBoundingClientRect()
      const x = event.clientX - rect.left
      const y = event.clientY - rect.top
      let foundSlice = null
      // 从最上层（最后绘制）的扇区开始检测
      const reversedSlices = [...this.slices].reverse()

      for (const slice of reversedSlices) {
        const hitPath = this.getSliceHitPath(slice)
        if (this.ctx.isPointInPath(hitPath, x, y)) {
          foundSlice = slice
          break
        }
      }

      if (foundSlice) {
        if (this.hoveredSlice !== foundSlice) {
          this.hoveredSlice = foundSlice
          this.draw()
        }
        this.updateTooltip(foundSlice, event)
      } else {
        if (this.hoveredSlice) {
          this.hoveredSlice = null
          this.draw()
        }
        this.tooltip.visible = false
      }
    },
    handleMouseOut() {
      this.isMouseOver = false
      if (this.hoveredSlice) {
        this.hoveredSlice = null
        this.draw()
      }
      this.tooltip.visible = false
      this.startCarousel()
    },
    updateTooltip(slice, event) {
      if (!slice) return
      this.tooltip.visible = true
      this.tooltip.name = slice.name
      this.tooltip.value = slice.value
      this.tooltip.color = slice.color

      if (event) {
        const rect = this.canvas.getBoundingClientRect()
        this.tooltip.top = event.clientY - rect.top + 15
        this.tooltip.left = event.clientX - rect.left
      } else {
        const { width, height } = this.$el.getBoundingClientRect()
        const radius = Math.min(width, height) * this.radiusScale
        const centerX = width / 2
        const centerY = height / 2 + this.maxPieHeight
        const yScale = this.perspective
        const topY = centerY - slice.height
        this.tooltip.left = centerX + Math.cos(slice.midAngle) * radius * 0.7
        this.tooltip.top = topY + Math.sin(slice.midAngle) * radius * yScale * 0.7
      }
    },
    startCarousel() {
      if (!this.enableCarousel || this.carouselTimer || this.isMouseOver) return
      this.carouselTimer = setInterval(() => {
        if (this.slices.length === 0) return
        this.carouselIndex = (this.carouselIndex + 1) % this.slices.length
        // 在轮播时，需要从原始（未排序）的数组中按顺序选择
        const originalSlices = this.slices.sort((a, b) => a.startAngle - b.startAngle)
        const slice = originalSlices[this.carouselIndex]
        this.hoveredSlice = slice
        this.updateTooltip(slice, null)
        this.draw()
      }, this.carouselInterval)
    },
    stopCarousel() {
      clearInterval(this.carouselTimer)
      this.carouselTimer = null
    }
  }
}
</script>

<style scoped>
.canvas-pie-container {
  width: 100%;
  height: 100%;
  position: relative;
}

canvas {
  display: block;
  cursor: pointer;
}

.tooltip {
  position: absolute;
  background-color: rgba(0, 0, 0, 0.75);
  border: 1px solid #333;
  border-radius: 4px;
  color: white;
  padding: 8px 12px;
  font-size: 14px;
  pointer-events: none;
  transform: translate(-50%, 10px);
  white-space: nowrap;
  transition: opacity 0.2s, top 0.2s, left 0.2s;
}

.tooltip-title {
  display: flex;
  align-items: center;
  margin-bottom: 4px;
}

.tooltip-legend {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin-right: 8px;
  display: inline-block;
}

.tooltip-name {
  font-weight: bold;
}
</style>